package sk.stuba.fiit.ltasys.web.mvc.model.service.core;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.xml.bind.JAXBElement;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampResponse;
import org.etsi.uri._01903.v1_3.CertificateValuesType;
import org.etsi.uri._01903.v1_3.EncapsulatedPKIDataType;
import org.etsi.uri._01903.v1_3.QualifyingPropertiesType;
import org.etsi.uri._01903.v1_3.XAdESTimeStampType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;
import org.springframework.stereotype.Service;
import org.w3._2000._09.xmldsig.ObjectType;
import org.w3._2000._09.xmldsig.ReferenceType;
import org.w3._2000._09.xmldsig.SignedInfoType;
import org.w3._2000._09.xmldsig.X509DataType;

import sk.stuba.fiit.ltasys.web.mvc.model.service.CryptographyService;
import sk.stuba.fiit.ltasys.ws.schema.beans.SignatureRequest;
import sk.stuba.fiit.ltasys.ws.schema.beans.Xadestype;

@Service("signatureVerifier")
public class SignatureVerifier {

	@Autowired
	Canonicalize canonicalize;
	@Autowired
	private Jaxb2Marshaller marshallerService;
	@Autowired
	private CryptographyService cryptographyService;
	
	
	public static final String SIGNED_PROPERTIES_TYPE = "http://uri.etsi.org/01903#SignedProperties";
	

	public boolean verifySignature(SignatureRequest signatureRequest, List<String> files) throws Exception {
		Xadestype signReqCan= null;
		try {
			SignatureRequest	rsignReqCan = canonicalize.canonicalize(signatureRequest);
		} catch (Exception e) {
			signReqCan = signatureRequest.getXades().get(0);
		}

		List<ReferenceType> reference = signReqCan.getSignature()
				.getSignedInfo().getReference();

		int countRef = reference.size();
		int countDat = files.size();
		byte[] hashRef = null;
		byte[] hashDat = null;

		List<Object> hashesToCompute = new ArrayList<Object>();
		for (ReferenceType referenceType : reference) {
			if (!(referenceType.getType() != null && referenceType.getType()
					.equals(SIGNED_PROPERTIES_TYPE))) {

				for (int i = 0; i < files.size(); i++) {
					File file = new File(files.get(i));
					if (referenceType.getURI().equals(file.getName())) {
						countRef--;
						countDat--;
						hashRef = referenceType.getDigestValue();
						hashDat = cryptographyService.calculateHash(cryptographyService.readFile(file),
								referenceType.getDigestMethod().getAlgorithm());
					}
				}
			} else {
				StringWriter writer = new StringWriter();
				Result result = new StreamResult(writer);
				ObjectType ot = signatureRequest.getXades().get(0)
						.getSignature().getObject().get(0);
				JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot
						.getContent().get(0);
				QualifyingPropertiesType qpt = jaxbelement.getValue();
				marshallerService.marshal(qpt.getSignedProperties(), result);
				hashDat = cryptographyService.calculateHash(writer.toString()
						.getBytes(), referenceType.getDigestMethod()
						.getAlgorithm());
				hashRef = referenceType.getDigestValue();
				countRef--;
			}
			hashesToCompute.add(hashDat);
		}
		String signAlgorithm = signatureRequest.getXades().get(0)
				.getSignature().getSignedInfo().getSignatureMethod()
				.getAlgorithm();

		@SuppressWarnings("unchecked")
		JAXBElement<X509DataType> jaxbdataType = (JAXBElement<X509DataType>) signatureRequest
				.getXades().get(0).getSignature().getKeyInfo().getContent()
				.get(0);
		X509DataType datatype = jaxbdataType.getValue();
		@SuppressWarnings("unchecked")
		JAXBElement<CertificateValuesType> jaxbCertValType = (JAXBElement<CertificateValuesType>) datatype
				.getX509IssuerSerialOrX509SKIOrX509SubjectName().get(0);
		CertificateValuesType certValType = jaxbCertValType.getValue();
		EncapsulatedPKIDataType epkidt = (EncapsulatedPKIDataType) certValType
				.getEncapsulatedX509CertificateOrOtherCertificate().get(0);
		CertificateFactory certFactory = CertificateFactory
				.getInstance("X.509");
		InputStream in = new ByteArrayInputStream(epkidt.getValue());
		X509Certificate certificate = (X509Certificate) certFactory
				.generateCertificate(in);

		if (!cryptographyService.verifieSignature(signAlgorithm, certificate,
				hashesToCompute, signatureRequest.getXades().get(0)
						.getSignature().getSignatureValue().getValue()))
			throw new Exception("Verification faild");
		if (!(countDat == countRef+1 && countRef + countDat == 1))
			return false;
		return true;
	}

	public void verifyTimestamp(SignatureRequest signatureRequest) throws Exception {
		ObjectType ot = signatureRequest.getXades().get(0).getSignature()
				.getObject().get(0);
		@SuppressWarnings("unchecked")
		JAXBElement<QualifyingPropertiesType> jaxbelement = (JAXBElement<QualifyingPropertiesType>) ot
				.getContent().get(0);
		QualifyingPropertiesType qpt = jaxbelement.getValue();

		EncapsulatedPKIDataType edtallDataTS = (EncapsulatedPKIDataType) qpt
				.getSignedProperties().getSignedDataObjectProperties()
				.getAllDataObjectsTimeStamp().get(0)
				.getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse allDataTimestamp = new TimeStampResponse(edtallDataTS.getValue());

		
		//ziskanie dat 
		List<ReferenceType> reference = signatureRequest.getXades().get(0).getSignature().getSignedInfo().getReference();
		int size = 0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(SIGNED_PROPERTIES_TYPE)){
				size += reference.get(i).getDigestValue().length;
			}
		}
		byte[] data = new byte[size];
		int index=0;
		for(int i=0; i < reference.size(); i++){
			if(reference.get(i).getType() == null || !reference.get(i).getType().equals(SIGNED_PROPERTIES_TYPE)){
				byte[]tmp = reference.get(i).getDigestValue();
				for(int j = 0; j<tmp.length; j++){
					data[index++]=tmp[j];
				}
			}
		}
		
		MessageDigest hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(data);
		if(!Arrays.equals(hash.digest(),allDataTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect allDataObjectTimestamp");
		}
				
		EncapsulatedPKIDataType edtinitSigDataTS;
		TimeStampResponse initialSignatureTimestamp;
		if (qpt.getSignedProperties().getSignedDataObjectProperties()
				.getAllDataObjectsTimeStamp().size() > 1) {
			 	index = qpt.getSignedProperties()
					.getSignedDataObjectProperties()
					.getAllDataObjectsTimeStamp().size() - 1;
			edtinitSigDataTS = (EncapsulatedPKIDataType) qpt
					.getSignedProperties().getSignedDataObjectProperties()
					.getAllDataObjectsTimeStamp().get(index)
					.getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
			initialSignatureTimestamp = new TimeStampResponse(		edtinitSigDataTS.getValue());
		} else {
			initialSignatureTimestamp = allDataTimestamp;
		}		
		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(data);
		if(!Arrays.equals(hash.digest(),initialSignatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect initialTimestamp");
		}
		

		@SuppressWarnings("unchecked")
		JAXBElement<XAdESTimeStampType> xdssigDataTS = (JAXBElement<XAdESTimeStampType>) qpt
				.getUnsignedProperties()
				.getUnsignedSignatureProperties()
				.getCounterSignatureOrSignatureTimeStampOrCompleteCertificateRefs()
				.get(0);
		EncapsulatedPKIDataType edtSigDataTS = (EncapsulatedPKIDataType) xdssigDataTS
				.getValue().getEncapsulatedTimeStampOrXMLTimeStamp().get(0);
		TimeStampResponse signatureTimestamp = new TimeStampResponse(
				edtSigDataTS.getValue());

		hash.reset();
		hash = MessageDigest.getInstance("SHA1", "BC");
		hash.update(signatureRequest.getXades().get(0).getSignature().getSignatureValue().getValue());
		if(!Arrays.equals(hash.digest(),signatureTimestamp.getTimeStampToken().getTimeStampInfo().getMessageImprintDigest())){
			throw new Exception("Incorrect SignatureTimestamp");
		}
		
		verifyAllSignatureTimestamp(allDataTimestamp, signatureTimestamp, signatureTimestamp);
	}
	
	private void verifyAllSignatureTimestamp(TimeStampResponse allDataTS,TimeStampResponse initTS,TimeStampResponse signTS) throws Exception{	
		if(initTS == null){
			if(!(allDataTS.getTimeStampToken().getTimeStampInfo().getGenTime().before(signTS
					.getTimeStampToken().getTimeStampInfo().getGenTime()))
					 ){
				throw new Exception("Timestamp are not corect");
			}
		}else{
			if(allDataTS.getTimeStampToken().getTimeStampInfo().getGenTime().after(initTS
					.getTimeStampToken().getTimeStampInfo().getGenTime()) ||  initTS
					.getTimeStampToken().getTimeStampInfo().getGenTime().after(signTS
					.getTimeStampToken().getTimeStampInfo().getGenTime())
					 ){
				throw new Exception("Timestamp are not corect");
			}
		}
	}
	
	
}
